package local

import (
	"context"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"strings"

	"github.com/filecoin-project/lotus/api/v0api"

	"github.com/filecoin-project/lotus/api"
	"github.com/urfave/cli/v2"
	"golang.org/x/xerrors"

	"github.com/filecoin-project/go-address"
	"github.com/filecoin-project/go-state-types/crypto"
	"github.com/filecoin-project/lotus/chain/types"
	"github.com/filecoin-project/lotus/lib/sigs"
)

// WalletNew creates a new address in the wallet with the given sigType.
func WalletNew(typ types.KeyType) (*types.KeyInfo, *address.Address, error) {
	ctyp := ActSigType(typ)
	if ctyp == crypto.SigTypeUnknown {
		return nil, nil, fmt.Errorf("unknown sig type: %s", typ)
	}

	pk, err := sigs.Generate(ctyp)
	if err != nil {
		return nil, nil, err
	}

	addr, err := WalletPrivateToAddress(ctyp, pk)
	if err != nil {
		return nil, nil, err
	}

	return &types.KeyInfo{
		Type:       typ,
		PrivateKey: pk,
	}, addr, nil
}

// WalletPrivateToAddress convert private key to public key address
func WalletPrivateToAddress(typ crypto.SigType, pk []byte) (*address.Address, error) {
	publicKey, err := sigs.ToPublic(typ, pk)
	if err != nil {
		return nil, err
	}

	var addr address.Address
	switch typ {
	case crypto.SigTypeSecp256k1:
		addr, err = address.NewSecp256k1Address(publicKey)
		if err != nil {
			return nil, fmt.Errorf("converting Secp256k1 to address: %w", err)
		}
	case crypto.SigTypeBLS:
		addr, err = address.NewBLSAddress(publicKey)
		if err != nil {
			return nil, fmt.Errorf("converting BLS to address: %w", err)
		}
	default:
		return nil, fmt.Errorf("unsupported key type: %v", typ)
	}

	return &addr, nil
}

// WalletSign signs the given bytes using the KeyType and private key.
func WalletSign(typ types.KeyType, pk []byte, data []byte) (*crypto.Signature, error) {
	return sigs.Sign(ActSigType(typ), pk, data)
}

// WalletVerify verify the signed message
func WalletVerify(sig *crypto.Signature, addr address.Address, msg []byte) error {
	return sigs.Verify(sig, addr, msg)
}

// WalletSignMessage signs the given message using the given private key.
func WalletSignMessage(typ types.KeyType, pk []byte, msg types.Message) (*types.SignedMessage, error) {
	mb, err := msg.ToStorageBlock()
	if err != nil {
		return nil, fmt.Errorf("serializing message: %w", err)
	}

	sig, err := WalletSign(typ, pk, mb.Cid().Bytes())
	if err != nil {
		return nil, fmt.Errorf("failed to sign message: %w", err)
	}

	return &types.SignedMessage{
		Message:   msg,
		Signature: *sig,
	}, nil
}

func WalletVerifyMessage(sm *types.SignedMessage) error {
	return WalletVerify(&sm.Signature, sm.Message.From, sm.Message.Cid().Bytes())
}

func ActSigType(typ types.KeyType) crypto.SigType {
	switch typ {
	case types.KTBLS:
		return crypto.SigTypeBLS
	case types.KTSecp256k1:
		return crypto.SigTypeSecp256k1
	default:
		return crypto.SigTypeUnknown
	}
}

// not have locked
func OfflineMessage(cctx *cli.Context, ctx context.Context, lpkFile string, nonce uint64, apiFullNode v0api.FullNode, msg *types.Message) (*types.SignedMessage, error) {
	inpdata, err := ioutil.ReadFile(lpkFile)
	if err != nil {
		return nil, err
	}

	var ki types.KeyInfo
	switch cctx.String("format") {
	case "hex-lotus":
		data, err := hex.DecodeString(strings.TrimSpace(string(inpdata)))
		if err != nil {
			return nil, err
		}

		if err := json.Unmarshal(data, &ki); err != nil {
			return nil, err
		}
	case "json-lotus":
		if err := json.Unmarshal(inpdata, &ki); err != nil {
			return nil, err
		}
	case "gfc-json":
		var f struct {
			KeyInfo []struct {
				PrivateKey []byte
				SigType    int
			}
		}
		if err := json.Unmarshal(inpdata, &f); err != nil {
			return nil, xerrors.Errorf("failed to parse go-filecoin key: %s", err)
		}

		gk := f.KeyInfo[0]
		ki.PrivateKey = gk.PrivateKey
		switch gk.SigType {
		case 1:
			ki.Type = types.KTSecp256k1
		case 2:
			ki.Type = types.KTBLS
		default:
			return nil, fmt.Errorf("unrecognized key type: %d", gk.SigType)
		}
	default:
		return nil, fmt.Errorf("unrecognized format: %s", cctx.String("format"))
	}
	msg.Nonce = nonce
	signedMessage, err := GasEstimateMessageGas(ctx, apiFullNode, ki, msg, nil)
	if err != nil {
		return nil, err
	}
	_, err = apiFullNode.MpoolPush(ctx, signedMessage)
	if err != nil {
		return nil, err
	}
	return signedMessage, nil

}

// not have locked
func OfflineMessagex(cctx *cli.Context, ctx context.Context, lpkFile string, nonce uint64, apiFullNode v0api.FullNode, msg *types.Message) (*types.SignedMessage, error) {
	inpdata, err := ioutil.ReadFile(lpkFile)
	if err != nil {
		return nil, err
	}

	var ki types.KeyInfo
	switch cctx.String("format") {
	case "hex-lotus":
		data, err := hex.DecodeString(strings.TrimSpace(string(inpdata)))
		if err != nil {
			return nil, err
		}

		if err := json.Unmarshal(data, &ki); err != nil {
			return nil, err
		}
	case "json-lotus":
		if err := json.Unmarshal(inpdata, &ki); err != nil {
			return nil, err
		}
	case "gfc-json":
		var f struct {
			KeyInfo []struct {
				PrivateKey []byte
				SigType    int
			}
		}
		if err := json.Unmarshal(inpdata, &f); err != nil {
			return nil, xerrors.Errorf("failed to parse go-filecoin key: %s", err)
		}

		gk := f.KeyInfo[0]
		ki.PrivateKey = gk.PrivateKey
		switch gk.SigType {
		case 1:
			ki.Type = types.KTSecp256k1
		case 2:
			ki.Type = types.KTBLS
		default:
			return nil, fmt.Errorf("unrecognized key type: %d", gk.SigType)
		}
	default:
		return nil, fmt.Errorf("unrecognized format: %s", cctx.String("format"))
	}
	msg.Nonce = nonce
	signedMessage, err := GasEstimateMessageGas1(ctx, apiFullNode, ki, msg, nil)
	if err != nil {
		return nil, err
	}
	_, err = apiFullNode.MpoolPush(ctx, signedMessage)
	if err != nil {
		return nil, err
	}
	return signedMessage, nil

}

// not have locked
func SignKeyInfo(cctx *cli.Context, lpkFile string) (*types.KeyInfo, error) {
	inpdata, err := ioutil.ReadFile(lpkFile)
	if err != nil {
		return nil, err
	}

	var ki types.KeyInfo
	switch cctx.String("format") {
	case "hex-lotus":
		data, err := hex.DecodeString(strings.TrimSpace(string(inpdata)))
		if err != nil {
			return nil, err
		}

		if err := json.Unmarshal(data, &ki); err != nil {
			return nil, err
		}
	case "json-lotus":
		if err := json.Unmarshal(inpdata, &ki); err != nil {
			return nil, err
		}
	case "gfc-json":
		var f struct {
			KeyInfo []struct {
				PrivateKey []byte
				SigType    int
			}
		}
		if err := json.Unmarshal(inpdata, &f); err != nil {
			return nil, xerrors.Errorf("failed to parse go-filecoin key: %s", err)
		}

		gk := f.KeyInfo[0]
		ki.PrivateKey = gk.PrivateKey
		switch gk.SigType {
		case 1:
			ki.Type = types.KTSecp256k1
		case 2:
			ki.Type = types.KTBLS
		default:
			return nil, fmt.Errorf("unrecognized key type: %d", gk.SigType)
		}
	default:
		return nil, fmt.Errorf("unrecognized format: %s", cctx.String("format"))
	}

	return &ki, nil

}

func GasEstimateMessageGas(ctx context.Context, apiFullNode v0api.FullNode, ki types.KeyInfo, msg *types.Message, spec *api.MessageSendSpec) (*types.SignedMessage, error) {
	cp := *msg
	msg = &cp
	inMsg := *msg

	fromA, err := apiFullNode.ResolveToKeyAddress(ctx, msg.From, nil)
	if err != nil {
		return nil, xerrors.Errorf("getting key address: %w", err)
	}

	msg, err = apiFullNode.GasEstimateMessageGas(ctx, msg, spec, types.EmptyTSK)
	if err != nil {
		return nil, xerrors.Errorf("GasEstimateMessageGas error: %w", err)
	}

	if msg.GasPremium.GreaterThan(msg.GasFeeCap) {
		inJson, _ := json.Marshal(inMsg)
		outJson, _ := json.Marshal(msg)
		return nil, xerrors.Errorf("After estimation, GasPremium is greater than GasFeeCap, inmsg: %s, outmsg: %s",
			inJson, outJson)
	}

	if msg.From.Protocol() == address.ID {
		fmt.Printf("Push from ID address (%s), adjusting to %s\n", msg.From, fromA)
		msg.From = fromA
	}

	b, err := apiFullNode.WalletBalance(ctx, msg.From)
	if err != nil {
		return nil, xerrors.Errorf("mpool push: getting origin balance: %w", err)
	}

	if b.LessThan(msg.Value) {
		return nil, xerrors.Errorf("mpool push: not enough funds: %s < %s", b, msg.Value)
	}
	sm, err := WalletSignMessage(ki.Type, ki.PrivateKey, *msg)
	if err != nil {
		return nil, err
	}
	return sm, nil
}

func GasEstimateMessageGas1(ctx context.Context, apiFullNode v0api.FullNode, ki types.KeyInfo, msg *types.Message, spec *api.MessageSendSpec) (*types.SignedMessage, error) {
	cp := *msg
	msg = &cp
	inMsg := *msg

	fromA, err := apiFullNode.ResolveToKeyAddress(ctx, msg.From, nil)
	if err != nil {
		return nil, xerrors.Errorf("getting key address: %w", err)
	}

	if msg.GasPremium.GreaterThan(msg.GasFeeCap) {
		inJson, _ := json.Marshal(inMsg)
		outJson, _ := json.Marshal(msg)
		return nil, xerrors.Errorf("After estimation, GasPremium is greater than GasFeeCap, inmsg: %s, outmsg: %s",
			inJson, outJson)
	}

	if msg.From.Protocol() == address.ID {
		fmt.Printf("Push from ID address (%s), adjusting to %s\n", msg.From, fromA)
		msg.From = fromA
	}

	b, err := apiFullNode.WalletBalance(ctx, msg.From)
	if err != nil {
		return nil, xerrors.Errorf("mpool push: getting origin balance: %w", err)
	}

	if b.LessThan(msg.Value) {
		return nil, xerrors.Errorf("mpool push: not enough funds: %s < %s", b, msg.Value)
	}
	sm, err := WalletSignMessage(ki.Type, ki.PrivateKey, *msg)
	if err != nil {
		return nil, err
	}
	return sm, nil
}
